<?php
/**
 * Nuwani PHP IRC Bot Framework
 * Copyright (c) 2006-2010 The Nuwani Project
 *
 * Nuwani is a framework for IRC Bots built using PHP. Nuwani speeds up bot
 * development by handling basic tasks as connection- and bot management, timers
 * and module managing. Features for your bot can easily be added by creating
 * your own modules, which will receive callbacks from the framework.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @copyright Copyright (c) 2006-2010 The Nuwani Project
 * @package Nuwani
 * @author Peter Beverloo <peter@lvp-media.com>
 * @author Dik Grapendaal <dik.grapendaal@gmail.com>
 * @author Paul Redert <paul@redert.nets>
 * @see http://nuwani.googlecode.com
 */

class Socket
{
        /**
         * These constants will be used to identify the kind of disconnection we
         * encountered in this Socket class while processing. It is advised to
         * always use these constants with the onDisconnect callbacks, since the
         * values may change in the future (if error codes overlap with socket
         * error codes, for example).
         */

        const     DISCONNECT_QUIT               = 1;
        const     DISCONNECT_TIMEOUT            = 2;
        const     DISCONNECT_SOCKET_EOF         = 3;
        const     DISCONNECT_SOCKET_READ_ERR    = 4;
        const     DISCONNECT_SOCKET_WRITE_ERR   = 5;

        /**
         * Contains the raw PHP socket which is used for communication over the
         * network towards the IRC server.
         *
         * @var resource
         */

        private $m_rSocket;

        /**
         * An array with basic information about the socket; the server we'll be
         * connecting with, port number, IP and port we'll locally bind to,
         * etcetera.
         *
         * @var array
         */

        // TODO make private again
        public $m_aSocketInfo;

        /**
         * Incase we receive incomplete messages from the IRC server, we should
         * buffer them and re-use them in a later cycle when they're completed.
         *
         * @var string
         */

        private $m_sRecvBuffer;

        /**
         * Contains the instance of the bot which owns us. This is used to send
         * the bot the callback for each message that's being received through
         * this socket.
         *
         * @var Bot
         */

        private $m_pBot;

        /**
         * Stores the last 10 incomming messages and the time they where sent.
         * @var array
         */
        private $m_aPacketsIn;

        /**
         * Stores the last 10 outgoing messages and the time they where sent.
         * @var array
         */
        private $m_aPacketsOut;

        /**
         * The constructor will initialise the default values, e.g. create the
         * socket that will be used for the connection. Actual connection will
         * be done later on.
         *
         * @param Bot $pBot The bot which eventually owns this socket.
         */

        public function __construct ($pBot)
        {
                $this -> m_pBot    = $pBot;
                $this -> m_rSocket = null;

                $this -> m_aSocketInfo = array
                (
                        'RemoteIP'      => '',
                        'RemotePort'    => 0,

                        'BindIP'        => null,

                        'Statistics'    => array
                (
                                'Packets' => array ('In' => 0, 'Out' => 0),
                                'Bytes'   => array ('In' => 0, 'Out' => 0)
                ),

                        'Context'       => stream_context_create (array ()),

                //'SSL'           => false

                );

                $this -> m_aRecvBuffer = '';
                $this -> m_aPacketsIn  = array ();
                $this -> m_aPacketsOut = array ();
        }

        /**
         * Defines the server which will be used for communication. This must be
         * an IP address, though we don't specifically check for it.
         *
         * @param string $sAddress IP Address of the server we're going to use.
         */

        public function setServer ($sAddress)
        {
                $this -> m_aSocketInfo ['RemoteIP'] = $sAddress;
        }

        /**
         * Port number of the IRC server. Usually this will be 6667, seeing 6697
         * usually gets used for secured connections which is supported as well.
         *
         * @param integer $nPort Port number we should be connecting to.
         */

        public function setPort ($nPort)
        {
                $this -> m_aSocketInfo ['RemotePort'] = $nPort;
        }

        /**
         * Sets the IP this socket should used to bind to locally, thus on our
         * own side of the connection. Some IRC servers limit the number of
         * connections that can be made from a single IP, this can be used to
         * have more.
         *
         * @param string $sIpAddress IP Address this socket will be bound to.
         */

        public function setBindTo ($sIpAddress)
        {
                $this -> m_aSocketInfo ['BindIP'] = $sIpAddress;
        }

        /**
         * For those of you paranoid of people listening on your connections,
         * Nuwani is one of the rare IRC bots that supports secured connections.
         * Feel free to enable it, however, think about updating the port too.
         *
         * @param boolean $bEnabled Should we be using a secured connection?
         */

        /**
        public function setSecuredConnection ($bEnabled)
        {
                       $this -> m_aSocketInfo ['SSL'] = $bEnabled;
        }
        */

        /**
         * The connect function will tell the socket to connect to the IRC
         * server and send the initialisation commands, being NICK and USER.
         * Returns a boolean telling you whether we connected successfully.
         * This method will automatically unregister the bot when the bot cannot
         * connect.
         *
         * @return boolean
         */

        public function connect ()
        {

                print_r($this -> m_aSocketInfo);

                echo "RemoteIP: " . $this -> m_aSocketInfo ['RemoteIP'] . PHP_EOL;

                if ($this -> m_aSocketInfo ['BindIP'] != null)
                {
                        stream_context_set_option ($this -> m_aSocketInfo ['Context'], 'socket', 'bindto', $this -> m_aSocketInfo ['BindIP']);
                }

                $sScheme = 'tcp';
                if ( extension_loaded ('openssl') ) // $this -> m_aSocketInfo ['SSL'] !== false && extension_loaded ('openssl'))
                {

                        //echo "m_aSocketInfo[SSL]: " . $this-> m_aSocketInfo ['SSL'] . PHP_EOL;

                        stream_context_set_option ($this -> m_aSocketInfo ['Context'], array
                        (
                                'ssl' => array
                                (
                                        'verify_peer'           => false,
                                        'allow_self_signed'     => true,
                                        'local_cert'            => __DIR__ . 'nuwani.pem',
                                        'passphrase'            => ''
                                 )
                        ));

                        //$this -> m_aSocketInfo ['RemotePort']; // = $this -> m_aSocketInfo ['SSL'];
                        $sScheme = 'ssl';
                }

                echo 'Connection string: ' . $sScheme . '://' . $this -> m_aSocketInfo ['RemoteIP'] . ':' . $this -> m_aSocketInfo ['RemotePort'] . PHP_EOL;

                $this -> m_rSocket = stream_socket_client ($sScheme . '://' .
                $this -> m_aSocketInfo ['RemoteIP'] . ':' . $this -> m_aSocketInfo ['RemotePort'],
                $nErrorNumber, $sErrorString, 2.0, STREAM_CLIENT_CONNECT, $this -> m_aSocketInfo ['Context']);

                if ($this -> m_rSocket !== false)
                {
                        $this -> setBlocking (false);
                        return true;
                }

                $this -> close ();

                echo '[Socket] Could not connect to "' . $this -> m_aSocketInfo ['RemoteIP'] . ':' . $this -> m_aSocketInfo ['RemotePort'] . '": ' .
                $sErrorString . ' (' . $nErrorNumber . ').' . PHP_EOL;

                return false;
        }

        /**
         * This method will close the socket if it's open and clean up some
         * variables.
         */

        private function close ()
        {
                if ($this -> m_rSocket !== null)
                {
                        fclose ($this -> m_rSocket);
                }

                $this -> m_aRecvBuffer = '';
                $this -> m_rSocket     = null;
        }

        /**
         * For some purposes we might want this socket to go in blocking mode,
         * therefore this function was implemented.
         *
         * @param boolean $bBlocking Should this be a blocking socket?
         * @return boolean
         */

        public function setBlocking ($bBlocking)
        {
                return stream_set_blocking ($this -> m_rSocket, ($bBlocking ? 1 : 0));
        }

        /**
         * This function will send a certain command directly to the server, no
         * buffer or anything will be applied on top of that.
         *
         * @param string $sCommand The command to send to the server.
         * @return boolean
         */

        public function send ($sCommand)
        {
                if (! $this -> m_rSocket)
                {
                        return false;
                }

                $sCommand = trim ($sCommand);

                if (count ($this -> m_aPacketsOut) == 10)
                {
                        array_shift ($this -> m_aPacketsOut);
                }

                array_push ($this -> m_aPacketsOut, microtime (true));

                $this -> m_aSocketInfo ['Statistics'] ['Packets'] ['Out'] ++;
                $this -> m_aSocketInfo ['Statistics'] ['Bytes']   ['Out'] += strlen ($sCommand);

                if (fwrite ($this -> m_rSocket, $sCommand . PHP_EOL) === false)
                {
                        $this -> onDisconnect (self :: DISCONNECT_SOCKET_WRITE_ERR);
                        return false;
                }

                if (strtoupper (substr ($sCommand, 0, 4)) == 'QUIT')
                {
                        echo '[Socket] ' . $this -> m_pBot ['Nickname'] . ' quit from ' . $this -> m_pBot ['Network'] . ' by command. Destroying the bot.' . PHP_EOL;

                        /** We don't want the bot reconnecting now. **/
                        $this -> onDisconnect (self :: DISCONNECT_QUIT);
                        BotManager :: getInstance () -> destroy ($this -> m_pBot);
                }

                return true;
        }

        /**
         * The process function will check the socket to see if any data can
         * be returned. If there is any, they will be returned using the Bot's
         * callback function (usually onReceive).
         *
         * @return boolean
         */

        public function process ()
        {
                if ($this -> m_rSocket === null)
                {
                        return false;
                }

                if (! $this -> m_rSocket || feof ($this -> m_rSocket))
                {
                        /** The bot died. **/
                        $this -> onDisconnect (self :: DISCONNECT_SOCKET_EOF);
                        return false;
                }

                $sIncoming = fread ($this -> m_rSocket, 2048);
                if ($sIncoming === false)
                {
                        $this -> onDisconnect (self :: DISCONNECT_SOCKET_READ_ERR);
                        return false;
                }

                $aIncoming = explode ("\n", ltrim ($this -> m_sRecvBuffer . $sIncoming));
                $this -> m_sRecvBuffer = array_pop ($aIncoming);

                foreach ($aIncoming as $sLine)
                {
                        $sLine = trim ($sLine);

                        if (count ($this -> m_aPacketsIn) >= 10)
                        {
                                array_shift ($this -> m_aPacketsIn);
                        }

                        array_push ($this -> m_aPacketsIn, microtime (true));

                        $this -> m_aSocketInfo ['Statistics'] ['Packets'] ['In'] ++;
                        $this -> m_aSocketInfo ['Statistics'] ['Bytes']   ['In'] += strlen ($sLine);

                        $this -> m_pBot -> onReceive ($sLine);
                }

                return true;
        }

        /**
         * The onDisconnect callback will clean up the Socket class by ensuring
         * the socket is really closed and resetting some variables to their
         * begin-states. After all that, we'll continue on to notify the Bot class
         * of the disconnection and why we've been disconnected. This class will
         * then take further action, if deemed necessary.
         *
         * @param integer $nReason An integer indicating the exact reason of disconnection.
         */

        public function onDisconnect ($nReason)
        {
                $this -> close ();

                /**
                 * Can't seem to retrieve the last error from stream_socket_* functions,
                 * so we'll have to make do with our own constants for now.
                 */
                return $this -> m_pBot -> onDisconnect ($nReason);
        }

        /**
         * This function will return the internal statistic array, so it can
         * be used for whatever purpose it will be used.
         *
         * @return array
         */

        public function getStatistics ()
        {
                return $this -> m_aSocketInfo ['Statistics'];
        }

        /**
         * Returns the load of this socket.
         *
         * @return integer
         */

        public function getLoad ()
        {
                $nTotalTime = 0;

                for ($i = 0; $i < count ($this -> m_aPacketsOut); $i ++)
                {
                        if ($i == (count ($this -> m_aPacketsOut) - 1))
                        {
                                $nTotalTime += time () - $this -> m_aPacketsOut [$i];
                        }
                        else
                        {
                                $nTotalTime += $this -> m_aPacketsOut [$i + 1] - $this -> m_aPacketsOut [$i];
                        }
                }

                return round (count ($this -> m_aPacketsOut / $nTotalTime), 2);
        }

};

?>